﻿using System;
using System.Collections.Generic;
using Glue.Converters;
using Glue.Internals.Nulls;

namespace Glue.Internals
{
    internal class ConverterContainer
    {
        private readonly Dictionary<Types,IConverter> converters = new Dictionary<Types, IConverter>();
        private readonly NullableTypeComparer nullableTypeComparer = new NullableTypeComparer();

        public bool AcceptNullablesAsNormalTypes { get; set; }
        
        public ConverterContainer()
        {
            AcceptNullablesAsNormalTypes = true;
        }

        public bool CanConvert(IObjectMember p1, IObjectMember p2)
        {
            var t1 = p1.MemberType;
            var t2 = p2.MemberType;
            return converters.ContainsKey(new Types(t1, t2)) || AreEqualPrimitives(p1, p2) || 
                ArePrimitiveCollectionsOfSameType(p1,p2) || AreCollectionsOfDifferentTypesButConverterExist(p1,p2) ||
                CanConvertAsNullable(t1, t2);
        }

        private bool AreEqualPrimitives(IObjectMember p1, IObjectMember p2)
        {
            return p1.MemberType == p2.MemberType && p1 is IPrimitiveMember && p2 is IPrimitiveMember;
        }

        private bool ArePrimitiveCollectionsOfSameType(IObjectMember p1, IObjectMember p2)
        {
            var collectionProperty1 = (p1 as ICollectionMember).AsMaybe();
            var collectionProperty2 = (p2 as ICollectionMember).AsMaybe();
            return collectionProperty1.HasValue() && collectionProperty2.HasValue() &&
                   collectionProperty1.GetValue().IsPrimitive() && collectionProperty2.GetValue().IsPrimitive() &&
                   collectionProperty1.GetValue().GetElementType().Equals(collectionProperty2.GetValue().GetElementType());
        }

        private bool AreCollectionsOfDifferentTypesButConverterExist(IObjectMember p1, IObjectMember p2)
        {
            var collectionProperty1 = (p1 as ICollectionMember).AsMaybe();
            var collectionProperty2 = (p2 as ICollectionMember).AsMaybe();
            return collectionProperty1.HasValue() && collectionProperty2.HasValue() &&
                converters.ContainsKey(
                    new Types(collectionProperty1.GetValue().GetElementType(),
                        collectionProperty2.GetValue().GetElementType()));
        }

        public void Add(IConverter convert)
        {
            converters.Add(new Types(convert.LeftType, convert.RightType), convert);
            if (convert.LeftType!=convert.RightType)
                converters.Add(new Types(convert.RightType, convert.LeftType), convert);
        }

        public IConverter Get(IObjectMember p1, IObjectMember p2, RelationDirection direction)
        {
            var t1 = p1.MemberType;
            var t2 = p2.MemberType;

            if (converters.ContainsKey(new Types(t1,t2)))
                return converters[new Types(t1, t2)];
            if (AreEqualPrimitives(p1,p2))
                return new SameTypeMapping(t1);
            if (ArePrimitiveCollectionsOfSameType(p1, p2))
            {
                return ValidateAndCreateCollectionMapping((ICollectionMember) p1, (ICollectionMember) p2,
                    new SameTypeMapping(((ICollectionMember) p2).GetElementType()), direction);
            }
            if (AreCollectionsOfDifferentTypesButConverterExist(p1, p2))
            {
                return ValidateAndCreateCollectionMapping((ICollectionMember) p1, (ICollectionMember) p2,
                    converters[new Types(((ICollectionMember) p1).GetElementType(),((ICollectionMember) p2).GetElementType())],
                    direction);
            }
            if (CanConvertAsNullable(t1, t2))
                return new NullableTypeMapper(t1, t2);
            throw new GlueException("No converter for types '"+t1.Name+"' and '"+t2.Name+"' have been added");
            
        }

        private IConverter ValidateAndCreateCollectionMapping(ICollectionMember member1, ICollectionMember member2, IConverter converter, RelationDirection direction)
        {
            var creatorsCreator = new CreatorsForCollectionMembers(member1, member2, converter, direction);
            var creators = creatorsCreator.AssertAndGetCreators();
            return new CollectionMapping(member1, member2, converter, creators);
        }

        private bool CanConvertAsNullable(Type t1, Type t2)
        {
            return AcceptNullablesAsNormalTypes && nullableTypeComparer.AreOneTheNullableVersionOfTheOther(t1, t2);
        }

        public IConverter ValidateConverter(IObjectMember leftProperty, IObjectMember rightProperty,
            IConverter converterToValidate, RelationDirection direction)
        {
            converterToValidate = HandleCollectionProperties(leftProperty, rightProperty, converterToValidate,direction);
            AssertConverterIsValid(leftProperty, rightProperty, converterToValidate);
            AssertNestedTypeIsValid(leftProperty, rightProperty, converterToValidate);
            return converterToValidate;
        }

        private IConverter HandleCollectionProperties(IObjectMember leftProperty, IObjectMember rightProperty, IConverter converter, RelationDirection direction)
        {
            var collectionMemberLeftMaybe = (leftProperty as ICollectionMember).AsMaybe();
            var collectionMemberRightMaybe = (rightProperty as ICollectionMember).AsMaybe();

            if (collectionMemberLeftMaybe.HasValue()  && collectionMemberRightMaybe.HasValue() && IsNotCollectionConverter(converter))
            {
                var collectionMemberLeft = collectionMemberLeftMaybe.GetValue();
                var collectionMemberRight = collectionMemberRightMaybe.GetValue();
                AssertConverterCorrectForCollection(collectionMemberLeft,collectionMemberRight,converter);
                return ValidateAndCreateCollectionMapping(collectionMemberLeft, collectionMemberRight, converter, direction);
            }
            return converter;
        }

        private void AssertConverterCorrectForCollection(ICollectionMember leftMember, ICollectionMember rightMember, IConverter converter)
        {
            if (!(leftMember.GetElementType() == converter.LeftType && rightMember.GetElementType() == converter.RightType) &&
                !(leftMember.GetElementType() == converter.RightType && rightMember.GetElementType() == converter.LeftType))
            {
                throw new GlueException("The properties '" + leftMember.Name + "' and '" + rightMember.Name + "' cannot be related with the converter '" + converter.GetType().Name + "', that converts between '" + converter.LeftType.Name + "' and '" + converter.RightType.Name + "'. " +
                    "Add a converter between '" + leftMember.GetElementType() + "' and '" + rightMember.GetElementType() + "'");
            }
        }

        private bool IsNotCollectionConverter(IConverter converter)
        {
            return !(converter is IMapBetweenCollections);
        }

        private void AssertConverterIsValid(IObjectMember leftProperty, IObjectMember rightProperty, IConverter converter)
        {
            if (!IsConverterValid(leftProperty, rightProperty, converter))
                throw new GlueException("The properties '" + leftProperty.Name + "' and '" + rightProperty.Name + "' cannot be related with the converter '" + converter.GetType().Name + "', that converts between '" + converter.LeftType.Name + "' and '" + converter.RightType.Name + "'");
        }

        private bool IsConverterValid(IObjectMember leftProperty, IObjectMember rightProperty, IConverter converter)
        {
            return (leftProperty.MemberType == converter.LeftType && rightProperty.MemberType == converter.RightType) ||
                   (leftProperty.MemberType == converter.RightType && rightProperty.MemberType == converter.LeftType);
        }

        private void AssertNestedTypeIsValid(IObjectMember leftProperty, IObjectMember rightProperty, IConverter converter)
        {
            if (IsNestedTypeWithWrongConverter(leftProperty, rightProperty, converter))
            {
                if (leftProperty.MemberType.Equals(rightProperty.MemberType))
                    throw new GlueException("You have added a nested relation between '" + leftProperty.Name + "' and '" + rightProperty.Name + "' without adding a nested mapping for this relation");
                throw new GlueException("The relation between '" + leftProperty.Name + "' and '" + rightProperty.Name + "' is a nested relation and needs a converter that holds a nested relation");
            }
        }

        private bool IsNestedTypeWithWrongConverter(IObjectMember leftProperty, IObjectMember rightProperty, IConverter converter)
        {
            return leftProperty is INestedMember && rightProperty is INestedMember && !(converter is IHoldNestedRelation);
        }

        private class Types
        {
            public Types(Type type1, Type type2)
            {
                Type1 = type1;
                Type2 = type2;
            }

            public Type Type1 { get; set; }
            public Type Type2 { get; set; }

            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != typeof (Types)) return false;
                return Equals((Types) obj);
            }

            public bool Equals(Types obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                return Equals(obj.Type1, Type1) && Equals(obj.Type2, Type2);
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    return ((Type1 != null ? Type1.GetHashCode() : 0)*397) ^ (Type2 != null ? Type2.GetHashCode() : 0);
                }
            }
        }
    }
}